package com.dimples.common.service;

import com.dimples.common.constant.DimplesConst;
import com.dimples.common.entity.SecurityUser;
import com.dimples.common.helper.RedisCache;
import com.dimples.common.properties.DimplesProperties;
import com.dimples.common.properties.TokenProperties;
import com.dimples.common.util.HttpContextUtil;

import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import javax.servlet.http.HttpServletRequest;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * @author zhongyj <1126834403@qq.com><br/>
 * @date 2021/6/26
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class DimplesJwtTokenServiceImpl {

    private static final Integer MILLIS_MINUTE_TEN = 20;
    public static final String CLAIM_CREATED = "created";
    public static final String CLAIM_SUB = "sub";

    private final DimplesProperties properties;
    private final RedisCache redisCache;

    /**
     * 根据 TokenDetail 生成 Token
     *
     * @param loginUser 用户信息
     * @return 生成的token
     */
    public String createToken(SecurityUser loginUser) {
        Map<String, Object> claims = CollUtil.newHashMap();
        claims.put(CLAIM_SUB, loginUser.getUsername());
        claims.put(CLAIM_CREATED, DateUtil.now());
        claims.put(DimplesConst.LOGIN_USER_KEY, UUID.fastUUID().toString());
        String token = this.createToken(claims);
        cacheToken(StrUtil.toString(claims.get(DimplesConst.LOGIN_USER_KEY)), JSONUtil.parseObj(loginUser));
        return token;
    }

    /**
     * 获取用户身份信息
     *
     * @return 用户信息
     */
    public SecurityUser getLoginUserFromToken(HttpServletRequest request) {
        // 获取请求携带的令牌
        String token = parseToken(request);
        if (StrUtil.isNotBlank(token)) {
            Claims claims = getClaimsFromToken(token);
            if (ObjectUtil.isEmpty(claims)) {
                return null;
            }
            // 解析对应的权限以及用户信息
            String jti = (String) claims.get(DimplesConst.LOGIN_USER_KEY);
            return getUserFromCache(jti);
        }
        return null;
    }

    /**
     * 从 token 中拿到 username
     *
     * @param request 请求头中获取的token
     * @return username
     */
    public String getUsernameFromToken(HttpServletRequest request) {
        String username;
        try {
            final Claims claims = this.getClaimsFromToken(parseToken(request));
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * token 是否过期
     * 验证令牌有效期，相差不足20分钟，自动刷新缓存
     *
     * @param securityUser HttpServletRequest
     */
    public void validateToken(SecurityUser securityUser) {
        DateTime expectExpireTime = DateUtil.offsetMinute(DateUtil.parseDateTime(DateUtil.now()), MILLIS_MINUTE_TEN);
        if (expectExpireTime.after(DateUtil.parseDateTime(DateUtil.now()))) {
            refreshToken(securityUser);
        }
    }

    /**
     * 缓存token
     *
     * @param key         UUID
     * @param userDetails 数据详情
     */
    public void cacheToken(String key, JSONObject userDetails) {
        String tokenKey = getTokenKey(key);
        redisCache.setCacheObject(tokenKey, userDetails, properties.getSecurity().getToken().getExpiration(), TimeUnit.MINUTES);
    }

    /**
     * 获取缓存Token Key
     *
     * @param key UUID
     * @return Token Key
     */
    public String getTokenKey(String key) {
        return StrUtil.startWith(key, DimplesConst.CACHE_KEY_TOKEN_PREFIX) ? key : DimplesConst.CACHE_KEY_TOKEN_PREFIX + key;
    }

    /**
     * 格式化Token，去除前面的Bearer
     *
     * @param token 原token
     * @return String
     */
    public String parseToken(String token) {
        if (StrUtil.isNotBlank(token) && token.startsWith(DimplesConst.TOKEN_PREFIX)) {
            token = token.replace(DimplesConst.TOKEN_PREFIX, "");
        }
        return token;
    }

    /**
     * 获取token中封装的信息
     *
     * @param token String
     * @return Claims
     */
    public Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser()
                    .setSigningKey(StrUtil.bytes(properties.getSecurity().getToken().getSecret(), StandardCharsets.UTF_8))
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

    /**
     * 移除redis中的token
     *
     * @param key UUID
     */
    public void removeToken(String key) {
        redisCache.deleteObject(getTokenKey(key));
    }

    /**
     * 刷新token信息
     *
     * @param securityUser 存储信息
     */
    private void refreshToken(SecurityUser securityUser) {
        securityUser.setExpireTime(DateUtil.formatDateTime(createExpirationDate()));
        cacheToken(getTokenKey(getClaimsFromToken(parseToken(HttpContextUtil.getRequest())).getId()), JSONUtil.parseObj(securityUser));
    }

    private String createToken(Map<String, Object> claims) {
        TokenProperties tokenProperties = properties.getSecurity().getToken();
        String token;
        try {
            token = Jwts.builder()
                    .setClaims(claims)
                    .setExpiration(this.createExpirationDate())
                    .signWith(SignatureAlgorithm.HS512, StrUtil.bytes(tokenProperties.getSecret(), StandardCharsets.UTF_8))
                    .compact();
        } catch (Exception ex) {
            //didn't want to have this method throw the exception, would rather log it and sign the token like it was before
            log.warn(ex.getMessage());
            token = Jwts.builder()
                    .setClaims(claims)
                    .setExpiration(this.createExpirationDate())
                    .signWith(SignatureAlgorithm.HS512, tokenProperties.getSecret())
                    .compact();
        }
        return token;
    }

    public String parseToken(HttpServletRequest request) {
        String header = properties.getSecurity().getToken().getHeader();
        String token = request.getHeader(header);
        if (StrUtil.isNotBlank(token) && token.startsWith(DimplesConst.TOKEN_PREFIX)) {
            token = token.replace(DimplesConst.TOKEN_PREFIX, "");
        }
        return token;
    }

    private SecurityUser getUserFromCache(String key) {
        String tokenKey = getTokenKey(key);
        Object user = redisCache.getCacheObject(tokenKey);
        if (ObjectUtil.isNotEmpty(user)) {
            return JSONUtil.toBean(JSONUtil.parseObj(user), SecurityUser.class);
        }
        return null;
    }

    private Date createExpirationDate() {
        TokenProperties tokenProperties = properties.getSecurity().getToken();
        return DateUtil.offsetMinute(DateUtil.parseDateTime(DateUtil.now()), tokenProperties.getExpiration());
    }

}
